/* * Copyright 2016-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.cloud.stream.config; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.junit.Test; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.cloud.stream.annotation.EnableBinding; import org.springframework.cloud.stream.annotation.Input; import org.springframework.cloud.stream.annotation.Output; import org.springframework.cloud.stream.annotation.StreamListener; import org.springframework.cloud.stream.binding.StreamListenerErrorMessages; import org.springframework.cloud.stream.messaging.Processor; import org.springframework.cloud.stream.messaging.Sink; import org.springframework.cloud.stream.test.binder.MessageCollector; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.integration.annotation.Router; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.support.DefaultMessageBuilderFactory; import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.MessagingException; import org.springframework.messaging.SubscribableChannel; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.util.Assert; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; import static org.springframework.cloud.stream.binding.StreamListenerErrorMessages.AMBIGUOUS_MESSAGE_HANDLER_METHOD_ARGUMENTS; import static org.springframework.cloud.stream.binding.StreamListenerErrorMessages.INPUT_AT_STREAM_LISTENER; import static org.springframework.cloud.stream.binding.StreamListenerErrorMessages.INVALID_DECLARATIVE_METHOD_PARAMETERS; import static org.springframework.cloud.stream.binding.StreamListenerErrorMessages.INVALID_INBOUND_NAME; import static org.springframework.cloud.stream.binding.StreamListenerErrorMessages.INVALID_OUTBOUND_NAME; import static org.springframework.cloud.stream.binding.StreamListenerErrorMessages.INVALID_OUTPUT_VALUES; import static org.springframework.cloud.stream.binding.StreamListenerErrorMessages.NO_INPUT_DESTINATION; import static org.springframework.cloud.stream.binding.StreamListenerErrorMessages.RETURN_TYPE_MULTIPLE_OUTBOUND_SPECIFIED; import static org.springframework.cloud.stream.binding.StreamListenerErrorMessages.RETURN_TYPE_NO_OUTBOUND_SPECIFIED; /** * @author Marius Bogoevici * @author Ilayaperumal Gopinathan */ public class StreamListenerHandlerMethodTests { @Test public void testInvalidInputOnMethod() throws Exception { try { SpringApplication.run(TestInvalidInputOnMethod.class, "--server.port=0"); fail("Exception expected: " + INPUT_AT_STREAM_LISTENER); } catch (BeanCreationException e) { assertThat(e.getCause().getMessage()).contains(INPUT_AT_STREAM_LISTENER); } } @Test public void testMethodWithObjectAsMethodArgument() throws Exception { ConfigurableApplicationContext context = SpringApplication.run(TestMethodWithObjectAsMethodArgument.class, "--server.port=0"); Processor processor = context.getBean(Processor.class); String id = UUID.randomUUID().toString(); final CountDownLatch latch = new CountDownLatch(1); final String testMessage = "testing"; processor.input().send(MessageBuilder.withPayload(testMessage).build()); MessageCollector messageCollector = context.getBean(MessageCollector.class); Message<?> result = messageCollector.forChannel(processor.output()).poll(1000, TimeUnit.MILLISECONDS); assertThat(result).isNotNull(); assertThat(result.getPayload()).isEqualTo(testMessage.toUpperCase()); context.close(); } @Test public void testStreamListenerMethodWithTargetBeanFromOutside() throws Exception { ConfigurableApplicationContext context = SpringApplication.run(TestStreamListenerMethodWithTargetBeanFromOutside.class, "--server.port=0"); Sink sink = context.getBean(Sink.class); final String testMessageToSend = "testing"; sink.input().send(MessageBuilder.withPayload(testMessageToSend).build()); DirectChannel directChannel = (DirectChannel) context.getBean(testMessageToSend.toUpperCase(), MessageChannel.class); MessageCollector messageCollector = context.getBean(MessageCollector.class); Message<?> result = messageCollector.forChannel(directChannel).poll(1000, TimeUnit.MILLISECONDS); sink.input().send(MessageBuilder.withPayload(testMessageToSend).build()); assertThat(result).isNotNull(); assertThat(result.getPayload()).isEqualTo(testMessageToSend.toUpperCase()); context.close(); } @Test public void testInvalidReturnTypeWithSendToAndOutput() throws Exception { try { SpringApplication.run(TestReturnTypeWithMultipleOutput.class, "--server.port=0"); fail("Exception expected: " + RETURN_TYPE_MULTIPLE_OUTBOUND_SPECIFIED); } catch (BeanCreationException e) { assertThat(e.getCause().getMessage()).contains(RETURN_TYPE_MULTIPLE_OUTBOUND_SPECIFIED); } } @Test public void testInvalidReturnTypeWithNoOutput() throws Exception { try { SpringApplication.run(TestInvalidReturnTypeWithNoOutput.class, "--server.port=0"); fail("Exception expected: " + RETURN_TYPE_NO_OUTBOUND_SPECIFIED); } catch (BeanCreationException e) { assertThat(e.getCause().getMessage()).contains(RETURN_TYPE_NO_OUTBOUND_SPECIFIED); } } @Test public void testInvalidInputAnnotationWithNoValue() throws Exception { try { SpringApplication.run(TestInvalidInputAnnotationWithNoValue.class, "--server.port=0"); fail("Exception expected: " + INVALID_INBOUND_NAME); } catch (BeanCreationException e) { assertThat(e.getCause().getMessage()).contains(INVALID_INBOUND_NAME); } } @Test public void testInvalidOutputAnnotationWithNoValue() throws Exception { try { SpringApplication.run(TestInvalidOutputAnnotationWithNoValue.class, "--server.port=0"); fail("Exception expected: " + INVALID_OUTBOUND_NAME); } catch (BeanCreationException e) { assertThat(e.getCause().getMessage()).contains(INVALID_OUTBOUND_NAME); } } @Test public void testMethodInvalidInboundName() throws Exception { try { SpringApplication.run(TestMethodInvalidInboundName.class, "--server.port=0"); fail("Exception expected on using invalid inbound name"); } catch (BeanCreationException e) { assertThat(e.getCause()).isInstanceOf(IllegalArgumentException.class); assertThat(e.getCause()).hasMessageContaining(StreamListenerErrorMessages.INVALID_DECLARATIVE_METHOD_PARAMETERS); } } @Test public void testMethodInvalidOutboundName() throws Exception { try { SpringApplication.run(TestMethodInvalidOutboundName.class, "--server.port=0"); fail("Exception expected on using invalid outbound name"); } catch (BeanCreationException e) { assertThat(e.getCause()).isInstanceOf(NoSuchBeanDefinitionException.class); assertThat(e.getCause()).hasMessageContaining("'invalid'"); } } @Test public void testAmbiguousMethodArguments1() throws Exception { try { SpringApplication.run(TestAmbiguousMethodArguments1.class, "--server.port=0"); fail("Exception expected: " + AMBIGUOUS_MESSAGE_HANDLER_METHOD_ARGUMENTS); } catch (BeanCreationException e) { assertThat(e.getCause().getMessage()).contains(AMBIGUOUS_MESSAGE_HANDLER_METHOD_ARGUMENTS); } } @Test public void testAmbiguousMethodArguments2() throws Exception { try { SpringApplication.run(TestAmbiguousMethodArguments2.class, "--server.port=0"); fail("Exception expected:" + AMBIGUOUS_MESSAGE_HANDLER_METHOD_ARGUMENTS); } catch (BeanCreationException e) { assertThat(e.getCause().getMessage()).contains(AMBIGUOUS_MESSAGE_HANDLER_METHOD_ARGUMENTS); } } @Test public void testMethodWithInputAsMethodAndParameter() throws Exception { try { SpringApplication.run(TestMethodWithInputAsMethodAndParameter.class, "--server.port=0"); fail("Exception expected: " + INVALID_DECLARATIVE_METHOD_PARAMETERS); } catch (BeanCreationException e) { assertThat(e.getCause().getMessage()).contains(INVALID_DECLARATIVE_METHOD_PARAMETERS); } } @Test public void testMethodWithOutputAsMethodAndParameter() throws Exception { try { SpringApplication.run(TestMethodWithOutputAsMethodAndParameter.class, "--server.port=0"); fail("Exception expected:" + INVALID_OUTPUT_VALUES); } catch (BeanCreationException e) { assertThat(e.getCause().getMessage()).startsWith(INVALID_OUTPUT_VALUES); } } @Test public void testMethodWithoutInput() throws Exception { try { SpringApplication.run(TestMethodWithoutInput.class, "--server.port=0"); fail("Exception expected when inbound target is not set"); } catch (BeanCreationException e) { assertThat(e.getCause().getMessage()).contains(NO_INPUT_DESTINATION); } } @Test public void testMethodWithMultipleInputParameters() throws Exception { ConfigurableApplicationContext context = SpringApplication.run(TestMethodWithMultipleInputParameters.class, "--server.port=0"); Processor processor = context.getBean(Processor.class); StreamListenerTestUtils.FooInboundChannel1 inboundChannel2 = context.getBean(StreamListenerTestUtils.FooInboundChannel1.class); String id = UUID.randomUUID().toString(); final CountDownLatch latch = new CountDownLatch(2); ((SubscribableChannel) processor.output()).subscribe(new MessageHandler() { @Override public void handleMessage(Message<?> message) throws MessagingException { Assert.isTrue(message.getPayload().equals("footesting") || message.getPayload().equals("BARTESTING")); latch.countDown(); } }); processor.input().send(MessageBuilder.withPayload("{\"foo\":\"fooTESTing\"}") .setHeader("contentType", "application/json").build()); inboundChannel2.input().send(MessageBuilder.withPayload("{\"bar\":\"bartestING\"}") .setHeader("contentType", "application/json").build()); assertThat(latch.await(1, TimeUnit.SECONDS)); context.close(); } @Test public void testMethodWithMultipleOutputParameters() throws Exception { ConfigurableApplicationContext context = SpringApplication.run(TestMethodWithMultipleOutputParameters.class, "--server.port=0"); Processor processor = context.getBean(Processor.class); String id = UUID.randomUUID().toString(); StreamListenerTestUtils.FooOutboundChannel1 source2 = context.getBean(StreamListenerTestUtils.FooOutboundChannel1.class); final CountDownLatch latch = new CountDownLatch(2); ((SubscribableChannel) processor.output()).subscribe(new MessageHandler() { @Override public void handleMessage(Message<?> message) throws MessagingException { Assert.isTrue(message.getPayload().equals("testing")); Assert.isTrue(message.getHeaders().get("output").equals("output2")); latch.countDown(); } }); ((SubscribableChannel) source2.output()).subscribe(new MessageHandler() { @Override public void handleMessage(Message<?> message) throws MessagingException { Assert.isTrue(message.getPayload().equals("TESTING")); Assert.isTrue(message.getHeaders().get("output").equals("output1")); latch.countDown(); } }); processor.input().send(MessageBuilder.withPayload("testING").setHeader("output", "output1").build()); processor.input().send(MessageBuilder.withPayload("TESTing").setHeader("output", "output2").build()); assertThat(latch.await(1, TimeUnit.SECONDS)); context.close(); } @EnableBinding({Processor.class, StreamListenerTestUtils.FooOutboundChannel1.class}) @EnableAutoConfiguration public static class TestMethodWithMultipleOutputParameters { @StreamListener public void receive(@Input(Processor.INPUT) SubscribableChannel input, @Output(Processor.OUTPUT) final MessageChannel output1, @Output(StreamListenerTestUtils.FooOutboundChannel1.OUTPUT) final MessageChannel output2) { input.subscribe(new MessageHandler() { @Override public void handleMessage(Message<?> message) throws MessagingException { if (message.getHeaders().get("output").equals("output1")) { output1.send(org.springframework.messaging.support.MessageBuilder.withPayload(message.getPayload().toString().toUpperCase()).build()); } else if (message.getHeaders().get("output").equals("output2")) { output2.send(org.springframework.messaging.support.MessageBuilder.withPayload(message.getPayload().toString().toLowerCase()).build()); } } }); } } @EnableBinding({Sink.class}) @EnableAutoConfiguration public static class TestMethodWithoutInput { @StreamListener public void receive(StreamListenerTestUtils.FooPojo fooPojo) { } } @EnableBinding({Processor.class}) @EnableAutoConfiguration public static class TestMethodWithObjectAsMethodArgument { @StreamListener(Processor.INPUT) @SendTo(Processor.OUTPUT) public String receive(Object received) { return received.toString().toUpperCase(); } } @EnableBinding(Sink.class) @EnableAutoConfiguration public static class TestStreamListenerMethodWithTargetBeanFromOutside { private static final String ROUTER_QUEUE = "routeInstruction"; @StreamListener(Sink.INPUT) @SendTo(ROUTER_QUEUE) public Message<String> convertMessageBody(Message<String> message) { return new DefaultMessageBuilderFactory().withPayload(message.getPayload().toUpperCase()).build(); } @Router(inputChannel = ROUTER_QUEUE) public String route(String message) { return message.toUpperCase(); } } @EnableBinding({Sink.class}) @EnableAutoConfiguration public static class TestInvalidInputOnMethod { @StreamListener @Input(Sink.INPUT) public void receive(StreamListenerTestUtils.FooPojo fooPojo) { } } @EnableBinding({Sink.class}) @EnableAutoConfiguration public static class TestAmbiguousMethodArguments1 { @StreamListener(Processor.INPUT) public void receive(@Payload StreamListenerTestUtils.FooPojo fooPojo, String value) { } } @EnableBinding({Sink.class}) @EnableAutoConfiguration public static class TestAmbiguousMethodArguments2 { @StreamListener(Processor.INPUT) public void receive(@Payload StreamListenerTestUtils.FooPojo fooPojo, @Payload StreamListenerTestUtils.BarPojo barPojo) { } } @EnableBinding({Processor.class, StreamListenerTestUtils.FooOutboundChannel1.class}) @EnableAutoConfiguration public static class TestReturnTypeWithMultipleOutput { @StreamListener public String receive(@Input(Processor.INPUT) SubscribableChannel input1, @Output(Processor.OUTPUT) MessageChannel output1, @Output(StreamListenerTestUtils.FooOutboundChannel1.OUTPUT) MessageChannel output2) { return "foo"; } } @EnableBinding({Processor.class, StreamListenerTestUtils.FooOutboundChannel1.class}) @EnableAutoConfiguration public static class TestInvalidReturnTypeWithNoOutput { @StreamListener public String receive(@Input(Processor.INPUT) SubscribableChannel input1) { return "foo"; } } @EnableBinding({Processor.class}) @EnableAutoConfiguration public static class TestInvalidInputAnnotationWithNoValue { @StreamListener public void receive(@Input SubscribableChannel input) { } } @EnableBinding({Processor.class}) @EnableAutoConfiguration public static class TestInvalidOutputAnnotationWithNoValue { @StreamListener public void receive(@Input(Processor.OUTPUT) SubscribableChannel input, @Output MessageChannel output) { } } @EnableBinding({Sink.class}) @EnableAutoConfiguration public static class TestMethodInvalidInboundName { @StreamListener public void receive(@Input("invalid") SubscribableChannel input) { } } @EnableBinding({Processor.class}) @EnableAutoConfiguration public static class TestMethodInvalidOutboundName { @StreamListener public void receive(@Input(Processor.INPUT) SubscribableChannel input, @Output("invalid") MessageChannel output) { } } @EnableBinding({Sink.class}) @EnableAutoConfiguration public static class TestMethodWithInputAsMethodAndParameter { @StreamListener public void receive(@Input(Sink.INPUT) StreamListenerTestUtils.FooPojo fooPojo) { } } @EnableBinding({Processor.class, StreamListenerTestUtils.FooOutboundChannel1.class}) @EnableAutoConfiguration public static class TestMethodWithOutputAsMethodAndParameter { @StreamListener @Output(StreamListenerTestUtils.FooOutboundChannel1.OUTPUT) public void receive(@Input(Processor.INPUT) SubscribableChannel input, @Output(Processor.OUTPUT) final MessageChannel output1) { input.subscribe(new MessageHandler() { @Override public void handleMessage(Message<?> message) throws MessagingException { output1.send(org.springframework.messaging.support.MessageBuilder.withPayload(message.getPayload().toString().toUpperCase()).build()); } }); } } @EnableBinding({Processor.class, StreamListenerTestUtils.FooInboundChannel1.class}) @EnableAutoConfiguration public static class TestMethodWithMultipleInputParameters { @StreamListener public void receive(@Input(Processor.INPUT) SubscribableChannel input1, @Input(StreamListenerTestUtils.FooInboundChannel1.INPUT) SubscribableChannel input2, final @Output(Processor.OUTPUT) MessageChannel output) { input1.subscribe(new MessageHandler() { @Override public void handleMessage(Message<?> message) throws MessagingException { output.send(org.springframework.messaging.support.MessageBuilder.withPayload(message.getPayload().toString().toUpperCase()).build()); } }); input2.subscribe(new MessageHandler() { @Override public void handleMessage(Message<?> message) throws MessagingException { output.send(org.springframework.messaging.support.MessageBuilder.withPayload(message.getPayload().toString().toUpperCase()).build()); } }); } } }